package alittlepond.world;

import static alittlepond.world.WorldInitProperty.*;
import static alittlepond.world.WorldProperty.*;

import java.awt.image.BufferedImage;
import java.util.Map;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;

import alittlepond.config.HasProperties;
import alittlepond.config.PropertyHaver;
import alittlepond.event.Notifiable;
import alittlepond.event.Notifier;
import alittlepond.event.NotifierAdapter;

/**
 * Singleton class holding the current state of the 2d world in which things live.
 * 
 * Initialised with dimensions and maybe a few other params.
 * 
 * Doesn't directly contain critters?
 * 
 * @author hopey
 *
 */
public enum World implements HasProperties<WorldProperty>, Notifier{
	INSTANCE;
	
	
	public enum WorldAction{
		NONE,
		BLOB,
		;
		
	}
	
	private PropertyHaver<WorldProperty> props = new PropertyHaver<WorldProperty>() {
		@Override
		public WorldProperty[] getPropertyDefinitions() {
			return WorldProperty.values();
		}
	};
	private PropertyHaver<WorldInitProperty> initProps = new PropertyHaver<WorldInitProperty>() {
		@Override
		public WorldInitProperty[] getPropertyDefinitions() {
			return WorldInitProperty.values();
		}
	};
	private NotifierAdapter notifier = new NotifierAdapter();
	private boolean initialised = false;
	public boolean isInitialised() {
		return initialised;
	}

	private int width, height;
	private double[][] values;

	private Random r;
	

	//TODO: create a configuration object or read a properties file to store all the parameters.
	public void init() {
		//TODO: prevent double initialisation
		//Actually, need the ability to reset it maybe.
		this.width = (Integer) initProps.getProperty(WIDTH);
		this.height = (Integer)initProps.getProperty(HEIGHT);

		System.out.println("Initialising world...\n dimensions " + width + " x " + height);
		
		
		values = new double[width][height];
		
		
		r = new Random();
		for(int i = 0; i< width; i++){
			for(int j = 0; j< height; j++) {
				values[i][j] = r.nextDouble();
			}
		}
		
		initialised = true;
		
		updateImage();


		int fps = (Integer) initProps.getProperty(FPS);
		int frameMs = (int) (1000.0 / fps + 0.5); //I hope that's right.
		

		new Timer(true).schedule(new TimerTask() {
			@Override
			public void run() {
				step();
			}

		},frameMs,frameMs);
		
	}

	private void step() {
		if((Boolean) getProperty(RUNNING)){
			blur(10);
			updateImage();
			notifier.updateAll();
		}
	}
	

	/*
	 * This all needs to be done in a non-swing specific way.
	 * 
	 * 
	 */
	private BufferedImage lastImage;
		
	public BufferedImage getImage() {
		return lastImage;
	}

	private void updateImage() {
		final BufferedImage newImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
		doSomethingForEachPoint(new PointExecutor() {

			@Override
			public void doSomething(int x, int y) {
				newImage.setRGB(x, y, getRgb(x, y));
			}
		});

		lastImage = newImage;
		
		
	}
	interface PointExecutor {
		void doSomething(int x, int y);
	}

	private int getRgb(int x, int y) {
		int v = (int) (values[x][y] * 256);

		return (0x0ff<<24| (v & 0x0ff) << 16) | ((v & 0x0ff) << 8) | (v & 0x0ff);
	}
	
	private void doSomethingForEachPoint(PointExecutor executor) {
		for (int y = 0; y < height; y++) {
			for (int x = 0; x < width; x++) {
				executor.doSomething(x, y);
			}
		}
	}

	


	public int getWidth() {
		return width;
	}



	public int getHeight() {
		return height;
	}



//	public double[][] getValues() {
//		return values;
//	}
	
	@Override
	public Map<WorldProperty, Object> getProperties() {
		return props.getProperties();
	}

	@Override
	public Object getProperty(WorldProperty property) {
		return props.getProperty(property);
	}

	@Override
	public WorldProperty[] getPropertyDefinitions() {
		return props.getPropertyDefinitions();
	}

	@Override
	public void setProperty(WorldProperty property, Object value) {
		props.setProperty(property, value);
	}

	public void addUpdateListener(Notifiable listener) {
		notifier.addUpdateListener(listener);
	}

	public void updateAll() {
		notifier.updateAll();
	}

	public void removeUpdateListener(Notifiable listener) {
		notifier.removeUpdateListener(listener);
	}
	
	private int getWrappedX(int x) {
		return wrapValue(x, width);
	}

	private int getWrappedY(int y) {
		return wrapValue(y, height);
	}

	private static int wrapValue(int value, int maxPlusOne) {
		if (value < 0) {
			return maxPlusOne + (value % maxPlusOne);
		}
		if (value >= maxPlusOne) {
			return value % maxPlusOne;
		}
		return value;
	}
	private void blur(int r) {


		double[][] tmp = blurHorizAndTranspose(values, width, height, (Integer) getProperty(BLUR_RADIUS));
		values = blurHorizAndTranspose(tmp, height, width, (Integer) getProperty(BLUR_RADIUS));
		final Double decayRate = (Double)getProperty(DECAY_RATE);

		doSomethingForEachPoint(new PointExecutor() {
			
			@Override
			public void doSomething(int x, int y) {
				values[x][y] = values[x][y] * decayRate;
			}
		});
	}

	private static double[][] blurHorizAndTranspose(double[][] in, int width, int height, int r) {
		int diam = 2 * r + 1;
		double[][] scaled = new double[width][height];
		double[][] outTransposed = new double[height][width];

		for (int x = 0; x < width; x++) {
			for (int y = 0; y < height; y++) {
				scaled[x][y] = in[x][y] / diam;
			}
		}

		
		
		for (int y = 0; y < height; ++y) {
			
			double total = 0;
			for (int i = -r; i <= r; ++i) {
				total += scaled[wrapValue(i, width)][y];
			}
			
			outTransposed[y][0] = total;
			for (int x = 1; x < width; ++x) {
				double oldValue = scaled[wrapValue(x - r -1, width)][y];
				double newValue = scaled[wrapValue(x + r, width)][y];
				total = total + newValue - oldValue;
				outTransposed[y][x] = total;
				

			}
		}
		return outTransposed;
	}
	public double getValue(int x, int y) {
		return values[getWrappedX(x)][getWrappedY(y)];
	}
	
	public int getX(int x) {
		return getWrappedX(x);
	}
	public int getY(int y) {
		return getWrappedY(y);
	}

	public HasProperties<WorldInitProperty> getInitProps() {
		return initProps;
	}

	public void clickAction(WorldAction action, int screenX, int screenY) {
		switch (action) {
		case BLOB:
			System.out.println("Blob at " + screenX + ", " + screenY);
			break;
		case NONE:
			System.out.println("None at " + screenX + ", " + screenY);
			break;

		default:
			break;
		}
	}
	
}
